Skip to content

Fix #14100: Ternary does not narrow type#4915

Merged
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-yih431k
Feb 13, 2026
Merged

Fix #14100: Ternary does not narrow type#4915
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-yih431k

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

Ternary expressions inside assert() (and other truthy contexts) did not narrow types. For example, assert($cond ? $x instanceof Foo : $x instanceof Bar) left $x unnarrowed, while the equivalent if/else with separate assert() calls worked correctly.

Changes

  • Modified the ternary handler in src/Analyser/TypeSpecifier.php (lines 1062-1072) to handle all truthy/falsey contexts, not just the special case where the else branch is false
  • The ternary $cond ? $a : $b is now converted to the equivalent ($cond && $a) || (!$cond && $b), which delegates to the existing BooleanOr/BooleanAnd narrowing logic
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14100.php covering:
    • Basic ternary with instanceof checks
    • Nested ternaries (three branches)
    • Scalar type checks (is_string/is_int)
    • Short ternary (Elvis) syntax

Root cause

The ternary handler in TypeSpecifier::specifyTypesInCondition() had two restrictive conditions:

  1. !$context->null() — excluded null context (though this didn't matter since AssertFunctionTypeSpecifyingExtension uses truthy context)
  2. $scope->getType($expr->else)->isFalse()->yes() — required the else branch to evaluate to false

This meant only the pattern $cond ? $expr : false was handled (converted to $cond && $expr). Any ternary where both branches carried meaningful type information (like $cond ? $x instanceof A : $x instanceof B) was silently ignored.

The fix removes the isFalse() restriction and generalizes the conversion to handle both branches, including the negated condition for the else branch.

Test

tests/PHPStan/Analyser/nsrt/bug-14100.php — NSRT test verifying that assert() with ternary conditions correctly narrows types. Covers instanceof, is_*() functions, nested ternaries, and short ternary syntax.

Fixes phpstan/phpstan#14100

ondrejmirtes and others added 2 commits February 13, 2026 06:23
- Generalized ternary handler in TypeSpecifier to handle all truthy contexts, not just the `$cond ? $expr : false` special case
- Converts `$cond ? $a : $b` to equivalent `($cond && $a) || (!$cond && $b)` so existing BooleanOr/BooleanAnd narrowing logic applies
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14100.php covering instanceof, is_string/is_int, nested ternaries, and short ternary syntax
- Root cause: the ternary handler required `$scope->getType($expr->else)->isFalse()->yes()` which excluded any ternary where the else branch wasn't literally `false`

Closes phpstan/phpstan#14100
Automated fix attempt 2 for CI failures.
@ondrejmirtes ondrejmirtes merged commit 2183ed7 into 2.1.x Feb 13, 2026
634 of 641 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-yih431k branch February 13, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ternary does not narrow type

2 participants